package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.hmdp.entity.Shop;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Function;

@RequiredArgsConstructor
@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    public void set(String key, Object value, Duration duration) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), duration);
    }

    public void setWithLogicalExpire(String key, Object value, Duration duration) {
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(duration.getSeconds()));
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    public <R, ID> R queryWithPassThrough(
            String keyPrefix,
            ID id,
            Class<R> type,
            Function<ID, R> dbFallback,
            Duration nullDuration,
            Duration duration
    ) {
        //存入redis的shopKey
        String key = keyPrefix + id;

        //从redis中获取shop
        String json = stringRedisTemplate.opsForValue().get(key);
        R rObject = null;

        //判断shopJSON是否为空，不为空直接就返回了
        if (StrUtil.isNotBlank(json)) {
            rObject = JSONUtil.toBean(json, type);
            log.info("缓存返回商品：[\n shop：{}\n shopJson：{}\n]", rObject, json);
            return rObject;
        }
        //判断命中的是否是空值
        if(json != null) {
            log.info("不存在指定目标[\ntype: {}\nid: {}\n]", type, id);
            return null;
        }

        //去数据库拿数据
        rObject = dbFallback.apply(id);
        //数据库都没得只能返回店铺不存在了
        if (rObject == null) {
            log.error("数据库中不存在：[\ntype: {}\nid: {}\n]", type, id);
            stringRedisTemplate.opsForValue().set(key, "", nullDuration);
            return null;
        }
        //因为redis里面没得，所以要存进去，存进去后就可以把数据传给前端了
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(rObject), duration);
        log.info("缓存中不存在，将{}写入缓存", rObject);
        return rObject;
    }

    /**
     * 采用逻辑过期方案查询，目的是为了解决缓存击穿问题
     * @param keyPrefix 根据id查询的key的前缀
     * @param id 根据id查询
     * @param lockKeyPrefix 锁的key的前缀
     * @param type json转为的目标类型
     * @return R 查到数据后返回，不存在则返回null
     * @param <R> 返回值类型
     */
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix,
            ID id,
            String lockKeyPrefix,
            Class<R> type,
            Function<ID, R> dbFallback,
            Duration duration
    ) {
        //存入redis的shopKey
        String key = keyPrefix + id;
        //有关shop互斥锁的key
        String lockKey = lockKeyPrefix + id;
        //是否拿到互斥锁
        boolean isLock = false;
        String json = null;
        R rObject = null;
        try {
            while(!isLock) {
                //尝试从缓存中获取json对象
                json = stringRedisTemplate.opsForValue().get(key);

                /*
                    由于每次都有个预热期，所以有做活动的话，就一定会在缓
                    存里面。缓存里面要是没有拿到数据，就证明一定不处于活动期间
                    所以直接返回null便可。
                */
                if (StrUtil.isBlank(json)) {
                    return null;
                }

                RedisData redisData = JSONUtil.toBean(json, RedisData.class);
                JSONObject data = (JSONObject) redisData.getData();
                rObject = JSONUtil.toBean(data, type);
                LocalDateTime expireTime = redisData.getExpireTime();
                //判断是否过期
                if (expireTime.isAfter(LocalDateTime.now())) {
                    //没有过期的话，直接返回数据
                    return rObject;
                }

                //如果过期的话，就去尝试拿锁
                isLock = tryLock(lockKey);

                //拿到锁后
                if (isLock) {
                    //先从缓存中再次尝试获取数据，确保不会出现多并发问题
                    json = stringRedisTemplate.opsForValue().get(key);
                    if (StrUtil.isBlank(json)) {
                        return null;
                    }
                    redisData = JSONUtil.toBean(json, RedisData.class);
                    data = (JSONObject) redisData.getData();
                    rObject = JSONUtil.toBean(data, type);
                    expireTime = redisData.getExpireTime();
                    //判断是否过期
                    if (expireTime.isAfter(LocalDateTime.now())) {
                        //没有过期的话，直接返回数据
                        return rObject;
                    }

                    CACHE_REBUILD_EXECUTOR.submit(() -> {
                        R dbResult = dbFallback.apply(id);
                        setWithLogicalExpire(key, dbResult, duration);
                        unlock(lockKey);
                    });
                }
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return rObject;
    }

    /**
     * 获取锁
     * @param key 锁的key
     * @return boolean 如果拿到锁了就返回true
     */
    private boolean tryLock(String key) {
        return BooleanUtil.isTrue(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofSeconds(RedisConstants.LOCK_SHOP_TTL)));
    }

    /**
     * 释放锁
     * @param key 锁的key
     */
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
}
